﻿using System;
using System.Collections.Generic;
using ValidationFramework.Reflection;

namespace ValidationFramework
{
	/// <summary>
	/// Holds and tests the validation rules for a property.
	/// </summary>
	public class ValidationPolicy : IValidationPolicy
	{
		private bool _locked;

		// --- Constructors

		/// <summary>
		/// Initializes a new instance of the ValidationPolicy class.
		/// </summary>
		internal ValidationPolicy(ICachedReflectionInfo memberReflectionInfo)
		{
			Guard.ArgumentNotNull(memberReflectionInfo, "memberReflectionInfo");
			MemberReflectionInfo = memberReflectionInfo;

			// default parent rules
			ParentScopeRules = new RuleCollection(memberReflectionInfo.MemberTypeHandle);

			// default memeber rules
			MemberScopeRules = new RuleCollection(memberReflectionInfo.MemberTypeHandle);

			_locked = false;
		}



		// --- Properties

		public RuleCollection ParentScopeRules { get; private set; }
		public RuleCollection MemberScopeRules { get; private set; }
		public ICachedReflectionInfo MemberReflectionInfo { get; private set; }

		// --- Properties | IValidation Policy
		public int RuleCount { get { return ParentScopeRules.Count + MemberScopeRules.Count; } }
		public int ParentRuleCount { get { return ParentScopeRules.Count; } }
		public int MemberRuleCount { get { return MemberScopeRules.Count; } }

		// --- IValidationPolicy Memebers
		public void Lock()
		{
			_locked = true;
		}

		public void UnLock()
		{
			_locked = false;
		}

		private bool IsWritable
		{
			get
			{
				return !_locked;
			}
		}

		public Rule AddRule(Rule rule, RuleScope scope)
		{
			EnsurePolicyIsWritable();

			if (scope == RuleScope.Parent)
				return ParentScopeRules.AddNew(rule);
			else
				return MemberScopeRules.AddNew(rule);
		}
		public void AddRules(RuleCollection rules, RuleScope scope)
		{
			foreach (var rule in rules)
			{
				AddRule(rule, scope);
			}
		}

		private void EnsurePolicyIsWritable()
		{
			if (!this.IsWritable)
				throw new InvalidOperationException("You cannot write to the validation policy when it is marked read-only");
		}

		public string MemberName
		{
			get
			{
				return MemberReflectionInfo.Name;
			}
		}

		/// <summary>
		/// Validates a property against the policy.
		/// </summary>
		/// <param name="parentValue">The value of the object containing the property to test</param>
		/// <param name="context">An arbitrary context defined by the consumer of the framework</param>
		/// <param name="ruleset">A filter to apply when testing rules.</param>
		/// <returns>A PropertyValidationError for every broken rule in the policy</returns>
		/// <exception cref="ArgumentNullException">row filter cannot be null</exception>
		public List<ValidationError> Validate(object parentValue, ValidationOptions options)
		{
			Guard.ArgumentNotNull(options, "options");

			// Check no rule option
			if (options.ThrowOnNoRulesFound)
			{
				var ruleCount = ParentScopeRules.Count + MemberScopeRules.Count;
				if (ruleCount <= 0)
				{
					throw new ValidationException("No Rules Found");
				}
			}

			// get property value using cached reflection
			var memberValue = MemberReflectionInfo.GetValue(parentValue);

			// perform validation
			var parentResults = TestParentScopeRules(parentValue, options);
			var valueResults = TestMemberScopeRules(memberValue, options);

			// aggregate and return errors
			var errors = new List<ValidationError>();
			errors.AddRange(CreateValidationErrorsFromResults(parentResults, memberValue, MemberReflectionInfo));
			errors.AddRange(CreateValidationErrorsFromResults(valueResults, memberValue, MemberReflectionInfo));

			if (options.ThrowExceptionOnError && errors.Count > 0)
			{
				throw new ValidationException("There were invalid items");
			}

			return errors;
		}

		/// <summary>
		/// Checks the supplied value against this policies rules ignoring rules scoped at the parent level.
		/// </summary>
		/// <param name="memberValue">The value of the member being tested</param>
		/// <param name="context">An arbitrary context defined by the consumer of the framework</param>
		/// <param name="ruleset">A filter to apply when testing rules. If null all rules will be tested</param>
		/// <returns>A list of validation errors for all the rules that were broken</returns>
		/// <remarks>This is used in thin client scenarios when we can't guarantee access to the parent object being built/edited.</remarks>
		public List<ValidationError> ValidateNoParent(object memberValue, ValidationOptions options)
		{
			// Check no rule option
			if (options.ThrowOnNoRulesFound)
			{
				if (MemberScopeRules.Count <= 0)
				{
					throw new ValidationException("No Rules Found");
				}
			}

			var results = TestMemberScopeRules(memberValue, options);
			var errors = CreateValidationErrorsFromResults(results, memberValue, MemberReflectionInfo);

			if (options.ThrowExceptionOnError && errors.Count > 0)
			{
				throw new ValidationException("There were invalid items");
			}

			return errors;
		}


		// --- Methods | Helper
		private List<ValidationResult> TestParentScopeRules(object parentValue, ValidationOptions options)
		{
			var results = new List<ValidationResult>();

			// test parent scoped rules
			foreach (var parentScopeRule in ParentScopeRules)
			{
				if (options.ShouldEvaluateRule(parentScopeRule))
				{
					// validate
					var result = parentScopeRule.Validate(new ValidationContext { Target = parentValue });

					results.Add(result);

					if (options.ThrowExceptionOnFirstInvalidItem && !result.PassedTest)
					{
						throw new ValidationException("An item was invalid");
					}
				}
			}

			return results;
		}

		private List<ValidationResult> TestMemberScopeRules(object memberValue, ValidationOptions options)
		{
			var results = new List<ValidationResult>();

			// test value scoped rules
			foreach (var valueRule in MemberScopeRules)
			{
				// skip if not in supplied ruleset.
				if (options.ShouldEvaluateRule(valueRule))
				{
					var result = valueRule.Validate(new ValidationContext { Target = memberValue });

					results.Add(result);

					if (options.ThrowExceptionOnFirstInvalidItem && !result.PassedTest)
					{
						throw new ValidationException("An item was invalid");
					}
				}
			}

			return results;
		}

		private List<ValidationError> CreateValidationErrorsFromResults(List<ValidationResult> results, object attemptedValue, ICachedReflectionInfo descriptor)
		{
			var errors = new List<ValidationError>();

			foreach (var result in results)
			{
				if (result.PassedTest)
				{
					continue;
				}

				var validationError = new ValidationError(result.Rule, result.ErrorMessage, attemptedValue, descriptor);
				errors.Add(validationError);
			}

			return errors;
		}


	}
}
